////////////////////////////////////////////////////////////////////////////////
//
//  Copyright (C) 2003-2006 Adobe Macromedia Software LLC and its licensors.
//  All Rights Reserved. The following is Source Code and is subject to all
//  restrictions on such code as contained in the End User License Agreement
//  accompanying this product.
//
////////////////////////////////////////////////////////////////////////////////

package mx.collections 
{
    
import flash.events.EventDispatcher;
import flash.events.IEventDispatcher;
import flash.utils.IDataInput;
import flash.utils.IDataOutput;
import flash.utils.IExternalizable;
import flash.utils.getQualifiedClassName;
import mx.core.IPropertyChangeNotifier;
import mx.core.mx_internal;
import mx.events.CollectionEvent;
import mx.events.CollectionEventKind;
import mx.events.PropertyChangeEvent;
import mx.events.PropertyChangeEventKind;
import mx.managers.ISystemManager;
import mx.managers.SystemManager;
import mx.resources.ResourceBundle;
import mx.utils.ArrayUtil;
import mx.utils.StringUtil;
import mx.utils.UIDUtil;

//--------------------------------------
//  Events
//--------------------------------------

/**
 *  Dispatched when the IList has been updated in some way.
 *  
 *  @eventType mx.events.CollectionEvent.COLLECTION_CHANGE
 */
[Event(name="collectionChange", type="mx.events.CollectionEvent")]

//--------------------------------------
//  Other metadata
//--------------------------------------

[ExcludeClass]

[RemoteClass(alias="flex.messaging.io.ArrayList")]

/**
 *  @private
 *  A simple implementation of IList that uses a backing Array.
 *  This base class will not throw ItemPendingErrors but it
 *  is possible that a subclass might.
 */
public class ArrayList extends EventDispatcher
	   implements IList, IExternalizable, IPropertyChangeNotifier
{
    include "../core/Version.as";

	//--------------------------------------------------------------------------
	//
	//  Class initialization
	//
	//--------------------------------------------------------------------------

	loadResources();
	
	//--------------------------------------------------------------------------
	//
	//  Class resources
	//
	//--------------------------------------------------------------------------

	[ResourceBundle("collections")]

    /**
	 *  @private
     */	
	private static var packageResources:ResourceBundle;

    /**
	 *  @private
     */	
	private static var resourceOutOfBounds:String;

	//--------------------------------------------------------------------------
	//
	//  Class methods
	//
	//--------------------------------------------------------------------------

    /**
	 *  @private    
     *  Loads resources for this class.
     */
	private static function loadResources():void
	{
		resourceOutOfBounds = packageResources.getString("outOfBounds");
	}
		
    //--------------------------------------------------------------------------
    //
    // Constructor
    // 
    //--------------------------------------------------------------------------

    /**
     *  Construct a new ArrayList using the specified array as its source.
     *  If no source is specified an empty array will be used.
     */
    public function ArrayList(source:Array=null)
    {
		super();

        disableEvents();
        this.source = source;
        enableEvents();
        _uid = UIDUtil.createUID();
    }
    
    //--------------------------------------------------------------------------
    //
    // Properties
    // 
    //--------------------------------------------------------------------------
    
    //----------------------------------
    // length
    //----------------------------------

    /**
     *  Get the number of items in the list.  An ArrayList should always
     *  know its length so it shouldn't return -1, though a subclass may 
     *  override that behavior.
     *
     *  @return int representing the length of the source.
     */
    public function get length():int
    {
    	if (source)
        	return source.length;
        else
        	return 0;
    }
    
    //----------------------------------
    // source
    //----------------------------------
    
    /**
     *  The source array for this ArrayList.  
     *  Any changes done through the IList interface will be reflected in the 
     *  source array.  
     *  If no source array was supplied the ArrayList will create one internally.
     *  Changes made directly to the underlying Array (e.g., calling 
     *  <code>theList.source.pop()</code> will not cause <code>CollectionEvents</code> 
     *  to be dispatched.
     *
	 *  @return An Array that represents the underlying source.
     */
    public function get source():Array
    {
        return _source;
    }
    
    public function set source(s:Array):void
    {
        var i:int;
        var len:int;
        if (_source && _source.length)
        {
            len = _source.length;
            for (i = 0; i < len; i++)
            {
                stopTrackUpdates(_source[i]);
            }
        }
        _source  = s ? s : [];
        len = _source.length;
        for (i = 0; i < len; i++)
        {
            startTrackUpdates(_source[i]);
        }
        
        if (_dispatchEvents == 0)
        {
           var event:CollectionEvent =
			new CollectionEvent(CollectionEvent.COLLECTION_CHANGE);
           event.kind = CollectionEventKind.RESET;
           dispatchEvent(event);
        }
    }
    
    //----------------------------------
    // uid -- mx.core.IPropertyChangeNotifier
    //----------------------------------
    
    /**
     *  Provides access to the unique id for this list.
     *  
     *  @return String representing the internal uid. 
     */  
    public function get uid():String
    {
    	return _uid;
    }
    
    public function set uid(value:String):void
    {
    	_uid = value;
	}

    //--------------------------------------------------------------------------
    //
    // Methods
    // 
    //--------------------------------------------------------------------------

    /**
     *  Get the item at the specified index.
     * 
     *  @param 	index the index in the list from which to retrieve the item
     *  @param	prefetch int indicating both the direction and amount of items
     *			to fetch during the request should the item not be local.
     *  @return the item at that index, null if there is none
     *  @throws ItemPendingError if the data for that index needs to be 
     *                           loaded from a remote location
     *  @throws RangeError if the index < 0 or index >= length
     */
    public function getItemAt(index:int, prefetch:int=0):Object
    {
        if (index < 0 || index >= length) 
        	throw new RangeError(StringUtil.substitute(resourceOutOfBounds, index));
            
        return source[index];
    }
    
    /**
     *  Place the item at the specified index.  
     *  If an item was already at that index the new item will replace it and it 
     *  will be returned.
     *
     *  @param 	item the new value for the index
     *  @param 	index the index at which to place the item
     *  @return the item that was replaced, null if none
     *  @throws RangeError if index is less than 0 or greater than or equal to length
     */
    public function setItemAt(item:Object, index:int):Object
    {
        if (index < 0 || index >= length) 
        	throw new RangeError(StringUtil.substitute(resourceOutOfBounds, index));
        
        var oldItem:Object = source[index];
        source[index] = item;
        stopTrackUpdates(oldItem);
        startTrackUpdates(item);
        
        //dispatch the appropriate events 
        if (_dispatchEvents == 0)
        {
        	var hasCollectionListener:Boolean = 
        		hasEventListener(CollectionEvent.COLLECTION_CHANGE);
        	var hasPropertyListener:Boolean = 
        		hasEventListener(PropertyChangeEvent.PROPERTY_CHANGE);
        	var updateInfo:PropertyChangeEvent; 
        	
        	if (hasCollectionListener || hasPropertyListener)
        	{
        		updateInfo = new PropertyChangeEvent(PropertyChangeEvent.PROPERTY_CHANGE);
        		updateInfo.kind = PropertyChangeEventKind.UPDATE;
            	updateInfo.oldValue = oldItem;
            	updateInfo.newValue = item;
            	updateInfo.property = index;
        	}
        	
        	if (hasCollectionListener)
        	{
           		var event:CollectionEvent =
					new CollectionEvent(CollectionEvent.COLLECTION_CHANGE);
            	event.kind = CollectionEventKind.REPLACE;
            	event.location = index;
            	event.items.push(updateInfo);
            	dispatchEvent(event);
         	}
         	
         	if (hasPropertyListener)
         	{
            	dispatchEvent(updateInfo);
          	}
        }
        return oldItem;    
    }
    
    /**
     *  Add the specified item to the end of the list.
     *  Equivalent to addItemAt(item, length);
     * 
     *  @param item the item to add
     */
    public function addItem(item:Object):void
    {
        addItemAt(item, length);
    }
    
    /**
     *  Add the item at the specified index.  
     *  Any item that was after this index is moved out by one.  
     * 
     *  @param item the item to place at the index
     *  @param index the index at which to place the item
     *  @throws RangeError if index is less than 0 or greater than the length
     */
    public function addItemAt(item:Object, index:int):void
    {
        if (index < 0 || index > length) 
        	throw new RangeError(StringUtil.substitute(resourceOutOfBounds, index));
        	
        source.splice(index, 0, item);

        startTrackUpdates(item);
        internalDispatchEvent(CollectionEventKind.ADD, item, index);
    }
    
    /**
     *  Return the index of the item if it is in the list such that
     *  getItemAt(index) == item.  
     *  Note that in this implementation the search is linear and is therefore 
     *  O(n).
     * 
     *  @param item the item to find
     *  @return the index of the item, -1 if the item is not in the list.
     */
    public function getItemIndex(item:Object):int
    {
    	return ArrayUtil.getItemIndex(item, source);
    }
    
    /**
     *  Removes the specified item from this list, should it exist.
     *
     *	@param	item Object reference to the item that should be removed.
     *  @return	Boolean indicating if the item was removed.
     */
    public function removeItem(item:Object):Boolean
    {
    	var index:int = getItemIndex(item);
    	var result:Boolean = index >=0;
    	if (result)
    		removeItemAt(index);

    	return result;
    }
    
    /**
     *  Remove the item at the specified index and return it.  
     *  Any items that were after this index are now one index earlier.
     *
     *  @param index the index from which to remove the item
     *  @return the item that was removed
     *  @throws RangeError is index < 0 or index >= length
     */
    public function removeItemAt(index:int):Object
    {
        if (index < 0 || index >= length)
        	throw new RangeError(StringUtil.substitute(resourceOutOfBounds, index));

        var removed:Object = source.splice(index, 1)[0];
        stopTrackUpdates(removed);
        internalDispatchEvent(CollectionEventKind.REMOVE, removed, index);
        return removed;
    }
    
    /** 
     *  Remove all items from the list.
     */
    public function removeAll():void
    {
        if (length > 0)
        {
            var len:int = length;
            for (var i:int = 0; i < len; i++)
            {
                stopTrackUpdates(source[i]);
            }

            source.splice(0, length);
			internalDispatchEvent(CollectionEventKind.RESET);
        }    
    }
    
    /**
     *  Notify the view that an item has been updated.  
     *  This is useful if the contents of the view do not implement 
     *  <code>IEventDispatcher</code>.  
     *  If a property is specified the view may be able to optimize its 
     *  notification mechanism.
     *  Otherwise it may choose to simply refresh the whole view.
     *
     *  @param item The item within the view that was updated.
	 *
     *  @param property A String, QName, or int
	 *  specifying the property that was updated.
	 *
     *  @param oldValue The old value of that property.
	 *  (If property was null, this can be the old value of the item.)
	 *
     *  @param newValue The new value of that property.
	 *  (If property was null, there's no need to specify this
	 *  as the item is assumed to be the new value.)
     *
     *  @see mx.events.CollectionEvent
     *  @see mx.core.IPropertyChangeNotifier
     *  @see mx.events.PropertyChangeEvent
     */
     public function itemUpdated(item:Object, property:Object = null, 
                                 oldValue:Object = null, 
                                 newValue:Object = null):void
    {
        var event:PropertyChangeEvent =
			new PropertyChangeEvent(PropertyChangeEvent.PROPERTY_CHANGE);
        
		event.kind = PropertyChangeEventKind.UPDATE;
        event.source = item;
        event.property = property;
        event.oldValue = oldValue;
        event.newValue = newValue;
        
		itemUpdateHandler(event);        
    }    
    
    /**
     *  Return an Array that is populated in the same order as the IList
     *  implementation.  
     * 
     *  @throws ItemPendingError if the data is not yet completely loaded
     *  from a remote location
     */ 
    public function toArray():Array
    {
        return source.concat();
    }
    
    /**
     *  Ensures that only the source property is seralized.
     *  @private
     */
    public function readExternal(input:IDataInput):void
    {
    	source = input.readObject();
    }
    
    /**
     *  Ensures that only the source property is serialized.
     *  @private
     */
    public function writeExternal(output:IDataOutput):void
    {
    	output.writeObject(_source);
    }

	/**
     *  Pretty prints the contents of this ArrayList to a string and returns it.
     */
    override public function toString():String
	{
		if (source)
			return source.toString();
		else
			return getQualifiedClassName(this);	
	}	
    
    //--------------------------------------------------------------------------
    //
    // Internal Methods
    // 
    //--------------------------------------------------------------------------

	/**
	 *  Enables event dispatch for this list.
	 */
	private function enableEvents():void
	{
		_dispatchEvents++;
		if (_dispatchEvents > 0)
			_dispatchEvents = 0;
	}
	
	/**
	 *  Disables event dispatch for this list.
	 *  To re-enable events call enableEvents(), enableEvents() must be called
	 *  a matching number of times as disableEvents().
	 */
	private function disableEvents():void
	{
		_dispatchEvents--;
	}
	
	/**
	 *  Dispatches a collection event with the specified information.
	 *
	 *  @param kind String indicates what the kind property of the event should be
	 *  @param item Object reference to the item that was added or removed
	 *  @param location int indicating where in the source the item was added.
	 */
	private function internalDispatchEvent(kind:String, item:Object = null, location:int = -1):void
	{
    	if (_dispatchEvents == 0)
    	{
    		if(hasEventListener(CollectionEvent.COLLECTION_CHANGE))
    		{
		        var event:CollectionEvent =
					new CollectionEvent(CollectionEvent.COLLECTION_CHANGE);
		        event.kind = kind;
		        event.items.push(item);
		        event.location = location;
		        dispatchEvent(event);
		    }

	    	// now dispatch a complementary PropertyChangeEvent
	    	if (hasEventListener(PropertyChangeEvent.PROPERTY_CHANGE) && 
	    	   (kind == CollectionEventKind.ADD || kind == CollectionEventKind.REMOVE))
	    	{
	    		var objEvent:PropertyChangeEvent =
					new PropertyChangeEvent(PropertyChangeEvent.PROPERTY_CHANGE);
	    		objEvent.property = location;
	    		if (kind == CollectionEventKind.ADD)
	    			objEvent.newValue = item;
	    		else
	    			objEvent.oldValue = item;
	    		dispatchEvent(objEvent);
	    	}
	    }
	}
	
    /**
     *  Called whenever any of the contained items in the list fire an
     *  ObjectChange event.  
     *  Wraps it in a CollectionEventKind.UPDATE.
     */    
    protected function itemUpdateHandler(event:PropertyChangeEvent):void
    {
		internalDispatchEvent(CollectionEventKind.UPDATE, event);
		// need to dispatch object event now
    	if (_dispatchEvents == 0 && hasEventListener(PropertyChangeEvent.PROPERTY_CHANGE))
    	{
    		var objEvent:PropertyChangeEvent = PropertyChangeEvent(event.clone());
    		var index:uint = getItemIndex(event.target);
    		objEvent.property = index.toString() + "." + event.property;
    		dispatchEvent(objEvent);
    	}
    }
    
    /** 
     *  If the item is an IEventDispatcher watch it for updates.  
     *  This is called by addItemAt and when the source is initially
     *  assigned.
     */
    protected function startTrackUpdates(item:Object):void
    {
        if (item && (item is IEventDispatcher))
        {
            IEventDispatcher(item).addEventListener(
				                        PropertyChangeEvent.PROPERTY_CHANGE, 
                                        itemUpdateHandler, false, 0, true);
        }
    }
    
    /** 
     *  If the item is an IEventDispatcher stop watching it for updates.
     *  This is called by removeItemAt, removeAll, and before a new
     *  source is assigned.
     */
    protected function stopTrackUpdates(item:Object):void
    {
        if (item && item is IEventDispatcher)
        {
            IEventDispatcher(item).removeEventListener(
				                        PropertyChangeEvent.PROPERTY_CHANGE, 
                                        itemUpdateHandler);    
        }
    }
    
    //--------------------------------------------------------------------------
    //
    // Variables
    // 
    //--------------------------------------------------------------------------

	/**
	 *  indicates if events should be dispatched.
	 *  calls to enableEvents() and disableEvents() effect the value when == 0
	 *  events should be dispatched. 
	 */
	private var _dispatchEvents:int = 0;
    private var _source:Array;
    private var _uid:String;
}

}
